home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Software Vault: The Gold Collection
/
Software Vault - The Gold Collection (American Databankers) (1993).ISO
/
cdr47
/
pctuto.zip
/
DISK4.EXE
/
lha
/
CHAP22-1.DOC
< prev
next >
Wrap
Text File
|
1990-07-28
|
31KB
|
693 lines
229
CHAPTER 22 - BCD NUMBERS
This chapter covers numbers which are in BCD format, both packed
and unpacked. You will probably never need to write any programs
on the 8086 that need these instructions, so you can either do
this chapter because it is the only time that you will run into
them, or you can skip the chapter because you will never see them
again. My advice would be to go through it anyways so you know
what the capabilities of the 8086 are. The programs in this
chapter are more advanced so will be more of a challenge to your
understanding.
BCD stands for binary coded decimals. In their unpacked form,
each byte stands for a single decimal digit. If we take a number
like 831974 and put it in memory, the bytes will look like this:
08h
03h
01h
09h
07h
04h
With high memory at the top and low memory at the bottom. Notice
that the top number is not ASCII '8' (hex 38), but the number 8
(hex 08). The same holds true for all the bytes; they are not
ASCII characters, they are numbers.
Why would we want to have numbers like this? They use up more
space (about 2 1/2 times as much), and the arithmetic operations
are slower (a multiplication on the 8086 can be several HUNDRED
times slower). They are not used for scientific operations. They
are used in business for billing. In a typical billing operation,
the number is entered from a terminal and stored. At the end of
the month, the number is printed on one line of the bill, and
then added to the total. If it were converted to an integer, it
would be necessary to do one conversion during data entry, then
another conversion during printing. This way, the only time it
will be converted is if it is used in some arithmetic.
One excuse for using BCD numbers is that they are more accurate.
It is true that they have no rounding errors, but neither do
integers. Integers and BCD numbers can have the same accuracy.
The typical form for BCD numbers is 18 digits. If you want to
convert an 18 digit number to a standard integer, it takes about
25 multiplications. That is a lot of multiplications to convert
one number. Similarly, it takes about 25 divisions to get a
number out of integer form back into a BCD number. This is a big
waste of time for a business situation. We keep the numbers in
decimal form and use them occasionally for arithmetic.
Actually, you would have to be crazy to use an 8086 for BCD
______________________
The PC Assembler Tutor - Copyright (C) 1989 Chuck Nelson
The PC Assembler Tutor 230
______________________
numbers. If you have a PC and use BCD numbers habitually, an 8087
coprocessor will work on BCD numbers at lightning speed. An
investment of $175 will save you countless hours of grief. Also,
it is next to impossible to do BCD division on the 8086, but
takes no longer than normal to do BCD division on the 8087.
That being said, just consider this chapter an academic exercise.
It will give you the information so if the question should arise
at a party, you will be able to say how BCD arithmetic works on
the 8086.
All 8086 numbers have the least significant digit in low memory
and the most significant digit in high memory. This includes
packed and unpacked BCD numbers. In the unpacked form, each byte
represents a digit, and has a value from 0 to 9. Here are some
numbers and their unpacked BCD encoding (in hex):
3986149 27 961728 74610
03h 02h 09h 07h
09h 07h 06h 04h
08h 01h 06h
06h 07h 01h
01h 02h 00h
04h 08h
09h
This wastes a lot of space. Someone early on noticed that the
upper half byte is completely unused. You can cut the space
consumption in half if you put two digits in each byte - one in
the lower half byte, and the other in the upper half byte. This
packing always starts from the least significant digit and goes
to the most significant digit. Here are the same numbers in
packed form.
3986149 27 961728 74610
03h 27h 96h 07h
98h 17h 46h
61h 28h 10h
49h
This is a considerable saving in space. The 8087 (not 8086)
standard for these numbers is 10 byte long numbers. The low order
9 bytes contain 18 digits. The last byte is 00h if the number is
positive and 80h if the number is negative. Here are an 18 digit
positive number and an 18 digit negative number, along with their
10 byte BCD encoding.
Chapter 22 - BCD Numbers 231
________________________
+137486298374691552 -581726405829645298
00 80
13 58
74 17
86 26
29 40
83 58
74 29
69 64
15 52
52 98
Let's work with the packed BCD numbers first.
PACKED BCD NUMBERS
The 8086 can manipulate packed BCD numbers. There is a subprogram
in asmhelp.obj that gets a BCD number from the keyboard and one
that prints a BCD number on the screen. Let's do some input and
output first.
; - - - - - - - - - - START DATA BELOW THIS LINE
variable1dw ? ; first four bcd digits
variable2dw ? ; second four bcd digits
variable3dw ? ; third four bcd digits
variable4dw ? ; fourth four bcd digits
variable5dw ? ; last two bcd digits and sign
; - - - - - - - - - - END DATA ABOVE THIS LINE
; - - - - - - - - - - START CODE BELOW THIS LINE
outer_loop:
lea ax, variable1
call get_bcd
mov ax, variable5
call print_hex
mov ax, variable4
call print_hex
mov ax, variable3
call print_hex
mov ax, variable2
call print_hex
mov ax, variable1
call print_hex
lea ax, variable1
call print_bcd
loop outer_loop
; - - - - - - - - - - END CODE ABOVE THIS LINE
This gets a 10 byte BCD number, prints it out in hex (with high
memory on top), and then prints the BCD number out again. Commas
The PC Assembler Tutor 232
______________________
are allowed. Printing the number in hex form allows you to see
what the numbers look like internally.
There are two instructions for BCD numbers - DAA (decimal adjust
for addition), and DAS (decimal adjust for subtraction). We'll
use each one on individual bytes to see how they work. Let's try
DAA.
; - - - - - - - - - - START CODE BELOW THIS LINE
mov ax_byte, 0C4h ; half registers, hex
mov bx_byte, 094h ; half regs signed, hex
mov dx_byte, 91h ; half registers, signed
lea ax, ax_byte
call set_reg_style
sub cx, cx ; clear cx for clarity
outer_loop:
mov ax, 0 ; clear the registers
mov bx, 0
mov dx, 0
call set_count
call show_regs
call get_hex_byte ; byte for al
mov dx, ax ; copy to dx
push ax ; save al
call get_hex_byte ; byte for bl
mov bl, al
mov bh, bl ; copy to bh
pop ax ; restore al
call show_regs_and_wait
add al, bl ; normal add
mov dx, ax ; copy to dx
call show_regs_and_wait
daa ; make adjustment
mov dx, ax ; copy to dx
call show_regs_and_wait
jmp outer_loop
; - - - - - - - - - - END CODE ABOVE THIS LINE
Since the program works with both BCD adjustments and integer
arithmetic, DX shows a signed integer copy of AX and BH shows a
signed integer copy of BL. A copy of AX is moved to DX every time
an operation is performed on AL.
The idea of this subprogram is to enter hex numbers that look
like decimal numbers - e.g. 65h, 78h, 08h, 29h. You can enter
numbers that contain A-F if you want to see what happens with bad
data. Each half byte of a BCD number should look like a decimal
number. You will notice that the actual addition is done by ADD.
The alteration is done by DAA, which assumes that you have just
added two legitimate BCD numbers, and the result is in AL. The
register MUST be AL. What DAA does will be discussed in a moment.
Chapter 22 - BCD Numbers 233
________________________
The DAA instruction looks at AL as being two half bytes. If the
result from the lower half byte addition is 10 or over, the 8086
subtracts 10 and then adds 1 to the upper half byte.{1} For
instance, if the result is 17, it subtracts 10, to leave 7 in the
lower half byte, and adds 1 to the upper half byte. After it has
done this, it looks at the upper half byte. If the upper half
byte is now 10 or over, it subtracts 10 and sets the carry flag
for future additions.{2}
While the whole thing sounds a little confusing, if you look at
the bytes in hex, they will act in the same way as if you were
doing normal decimal addition with pencil and paper except all
the carries are done at one time. CF will be set if the hundreds
digit is 1 and will be cleared if the hundreds digit is 0. Use
the previous program a few times to get the hang of what is going
on. When you feel confident, we'll move on to subtraction.
SUBTRACTION
DAS (decimal adjust for subtraction) is similar to DAA. It makes
an adjustment after the subtraction itself. We'll use the same
program as before, making two alterations. Where you have ADD,
change it to SUB ; where you have DAA, change it to DAS. That's
all.
add -> sub
daa -> das
Run the program, and put in a number of examples. If the lower
number is larger than the top number, there will be a borrow.
DAS works the opposite of DAA. If there has been a borrow into
the low half byte, it adds 10 to the low half byte and subtracts
1 from the high half byte. It then looks at the high half byte.
____________________
1. The technical description is a little confusing, so if you
get muddled up, just forget it. Here it goes. (AF is the
auxilillary flag). The 8086 checks for either (1) the low half
byte > 9 (that is, between 10 and 15) or (2) AF = 1. AF is set on
a byte addition when there is a carry out of the low half byte,
that is, the result is greater than 16 for the low half byte. If
either of these events has occured, the 8086 ADDS 6 to the low
half byte. Why? Let 10 + x represent the result of the addition,
where x < 10. We then have:
10 + x + 6 = 10 + 6 + x = 16 + x
but 16 is 0001 0000 (10h), the first bit in the high byte, so
this has the effect of leaving the part less than 10 in the low
half byte and adding 1 (the carry) to the high half byte.
2. This is exactly the same logic as in the last footnote,
except that the 8086 adds 60h to the high byte. This has the
effect of shifting the excess out of AL altogether. The 8086 then
sets the carry flag.
The PC Assembler Tutor 234
______________________
If there has been a borrow into it, the 8086 adds 10 to the high
half byte and sets the carry flag.
Do a few more examples with the program to make sure you
understand what's happening. It will look just like what you do
with pencil and paper, except that with pencil and paper you do
the borrow before you do the subtraction and here the borrow is
done afterwards if it is needed.
ADDITION AND SUBTRACTION
We have a number of possibilities with addition and subtraction.
Here they are. The sign of the numbers is in parentheses and the
operation is in between.
(+) + (+) (+) + (-) (+) - (+) (+) - (-)
(-) + (+) (-) + (-) (-) - (+) (-) - (-)
The subroutines we use are going to assume that (1) both numbers
are the same sign, and (2) for subtraction, the larger number is
on top and the smaller number is on the bottom. There should be a
section of the program that decides whether addition or
subtraction should be used, and which number is on top. Then
comes the addition or subtraction. We will write the first part
later. For now, when you input numbers, both numbers must have
the same sign, and for subtraction, the larger number must be
first. Here's the addition subroutine.
; - - - - - START SUBROUTINE BELOW THIS LINE
_bcd_addition proc near
RESULT_ADDRESS EQU [bp + 8]
BOTTOM_ADDRESS EQU [bp + 6]
TOP_ADDRESS EQU [bp + 4]
push bp
mov bp, sp
PUSHREGS ax, bx, cx, si, di
mov si, TOP_ADDRESS
mov bx, BOTTOM_ADDRESS
mov di, RESULT_ADDRESS
mov cx, 9 ; 9 bytes of BCD numbers
clc ; clear the carry flag
add_loop:
mov al, [si] ; move and add bytes
adc al, [bx] ; add two numbers and the carry
daa ; bcd adjust
mov [di], al ; store result
inc si ; increment pointers
inc bx
inc di
loop add_loop
jnc continue_addition ; BCD overflow if CF = 1
Chapter 22 - BCD Numbers 235
________________________
lea ax, overflow_message
call print_string
continue_addition:
mov al, [si] ; sign of top addend to result
mov [di], al
POPREGS ax, bx, cx, si, di
pop bp
ret
_bcd_addition endp
; - - - - END SUBROUTINE ABOVE THIS LINE
AL contains the first number. We use the carry flag (ADC) for
carrying from byte to byte. The carry flag is cleared before the
first addition since we don't want a carry there. The INC
instruction was designed by Intel so that it would not alter the
carry flag just so we could use it in situations like this.
If CF = 1 upon exiting the loop, the result was too large and we
had overflow. In this case we print a message to that effect.
The result [di] can be stored in the same place as one of the
numbers. The addition of each byte is completed before the result
for that byte is stored, so this won't interfere with the
addition. We assume that the sign of the result is the sign of
the top addend. (If this goes into a working program, it will
only be used if both numbers have the same sign).
This is designed as a C subroutine. The calling program pushes
things on the stack and it has the responsibility of popping them
off the stack on return from the call.
Here's the calling routine:
; - - - - - ENTER DATA BELOW THIS LINE
bcd_num1 dt ?
bcd_num2 dt ?
bcd_num3 dt ?
overflow_message db "We had overflow.", 0
; - - - - - ENTER DATA ABOVE THIS LINE
; - - - - - ENTER CODE BELOW THIS LINE
outer_loop:
lea ax, bcd_num1 ; get 2 numbers for addition
call get_bcd
lea ax, bcd_num2
call get_bcd
lea ax, bcd_num3 ; push addresses for subroutine
push ax
lea ax, bcd_num2
push ax
lea ax, bcd_num1
push ax
The PC Assembler Tutor 236
______________________
call _bcd_addition
add sp, 6 ; 3 pushes = 6 bytes
lea ax, bcd_num1 ; print both numbers and result
call print_bcd
lea ax, bcd_num2
call print_bcd
lea ax, bcd_num3
call print_bcd
loop outer_loop
; - - - - - ENTER CODE ABOVE THIS LINE
This is the main program. The other one should be in the
subroutine section. This program is straightforward. We get two
numbers, call the subroutine, and print the numbers and the
result. Try some numbers. The results you get will always have
the sign of the top number and will have the same numerical
result as adding two unsigned numbers.
The subtraction routine is the same as the addition but we need
to make some changes because it is subtraction:
ADC -> SBB
DAA -> DAS (decimal adjust for subtraction)
add_loop: -> subtract_loop
loop add_loop -> loop subtract_loop
jmp continue_addition -> jmp continue_subtraction
continue_addition: -> continue_subtraction:
Also, we want to change the subroutine name and call
_bcd_addition -> _bcd_subtraction
call _bcd_addition -> call _bcd_subtraction
Do all these changes, run the program, but make sure the top
number is larger or you will get strange results.
We now have an addition routine that only works with the right
numbers and a subtraction routine that is very touchy about the
numbers that are put in. How can these things help us? In fact
they are almost all we need. The only thing else we need is a
preliminary subroutine to organize what we do. To see why we have
some orginizational problems, take out a pencil and paper and do
the following additions and subtractions. Do these with a pencil
and paper, not a pocket calculator:
(+15) + (+27) (+15) + (-27)
(+15) - (+27) (+15) - (-27)
(-15) + (+27) (-15) + (-27)
(-15) - (+27) (-15) - (-27)
(+27) + (+15) (+27) + (-15)
(+27) - (+15) (+27) - (-15)
(-27) + (+15) (-27) + (-15)
(-27) - (+15) (-27) - (-15)
Chapter 22 - BCD Numbers 237
________________________
There are only four possible answers: +12, -12, +42 and -42. We
had 16 different additions and subtractions, yet we got only 4
possible answers and only 2 possible absolute values. We could
have a different subroutine for each one, but 16 subprograms is a
LOT of code, so it's easier to do it with 2 subprograms. We
merely need to order the numbers and pick the correct subroutine.
Here is the BCD driving routine. We'll get a number and then
we'll get an operation, either addition or subtraction. If it is
subtraction, when we get the second number, we REVERSE the sign.
We don't even check what it is; plus becomes minus and minus
becomes plus. What we have now is the ADDITION of two signed
numbers. We XOR the two signs. If the result is 0, they both are
the same sign and we can go to the addition subroutine. If the
signs are different we need to subtract the smaller from the
larger. We find out which one is larger and then call the
subtraction routine. It is going to take you a little time to
read this code.
; - - - - - START DATA BELOW THIS LINE
top_number dt ?
bottom_number dt ?
result dt ?
sign_mask db ?
sign_message db "Enter either + or -.", 0
overflow_message db "We had overflow.", 0
; - - - - - END DATA ABOVE THIS LINE
; - - - - - START CODE BELOW THIS LINE
outer_loop:
lea ax, top_number
call get_bcd
sign_loop:
; get either a '+' or a '-'
lea ax, sign_message ; prompt for operation
call print_string
call get_ascii_byte ; operation type in al
cmp al, '+'
jne check_for_minus
mov sign_mask, 00h ; for XOR of bottom number sign
jmp get_second_number
check_for_minus:
cmp al, '-'
jne sign_loop ; if not a minus then redo
mov sign_mask, 80h ; for XOR of bottom sign
get_second_number:
lea ax, bottom_number
call get_bcd
; XOR bottom sign with sign mask
mov al, sign_mask
xor BYTE PTR (bottom_number + 9), al ; sign byte
; same sign or different signs?
mov ah, BYTE PTR (top_number + 9) ; sign byte
The PC Assembler Tutor 238
______________________
xor ah, BYTE PTR (bottom_number + 9) ; different?
jnz which_is_larger ; if different, subtract
; same sign, so add
lea ax, result ; push parameters and add
push ax
lea ax, bottom_number
push ax
lea ax, top_number
push ax
call _bcd_addition
add sp, 6 ; adjust the stack
jmp print_the_numbers
which_is_larger:
lea si, top_number + 8 ; top digits
lea di, bottom_number + 8
mov cx, 9 ; 9 bytes of digits
check_for_greater_loop:
mov al, [si] ; top number
cmp al, [di] ; bottom number
ja top_is_more
jb bottom_is_more
dec si ; equal, so continue
dec di
loop check_for_greater_loop
; we fell through so they are the same
; leave the top number on top
top_is_more:
lea ax, result
push ax
lea ax, bottom_number
push ax
lea ax, top_number
push ax
call _bcd_subtraction
add sp, 6 ; adjust the stack
jmp print_the_numbers
bottom_is_more:
lea ax, result
push ax
lea ax, top_number
push ax
lea ax, bottom_number
push ax
call _bcd_subtraction
add sp, 6 ; adjust the stack
print_the_numbers:
lea ax, top_number
call print_bcd
lea ax, bottom_number
call print_bcd
lea ax, result
call print_bcd
Chapter 22 - BCD Numbers 239
________________________
jmp outer_loop
; - - - - - END CODE ABOVE THIS LINE
The sign_mask is either 00h if it is addition or 80h if it is
subtraction. 00h XOR sign_byte will leave the sign byte
unchanged. 80h XOR sign_byte will reverse the sign of the sign
byte.
Thus, as we did in the earlier multiplication and division
routines, we sometimes change the sign of a number
(bottom_number). In a real routine we would either have to make a
copy of it when we change the sign or change the sign back at the
end. Here we leave it with the sign change (if any) so you can
see what is happening internally. If you want to restore the
number, you can alter the code a little:
print_the_numbers:
mov al, sign_mask ; add this
xor BYTE PTR (bottom_number + 9), al ; add this
lea ax, top_number
This will restore the bottom number to its original form ASSUMING
THAT THE RESULT HAS NOT BEEN STORED THERE.
It is possible to get -0 as a result. This is a legally defined
number: +0 = -0.
There are three places where we have 'BYTE PTR'. This is because
the variables are defined as 10 byte long objects and the
assembler will complain if we don't put in the 'BYTE PTR'.
Finally, if we want to use the typical 'destination, source'
style for the 8086, it is built in. Here it is for addition:
lea ax, top_number
push ax
lea ax, bottom_number
push ax
lea ax, top_number
push ax
We put the address of 'top_number' where the result address goes.
The routines store a byte only after all calculations are done on
that particular byte, so there is no interference from doing
this.